Dans cette étude, nous travaillons sur des données issue du Vendée Globe 2020-2021. Les données constituant notre base sont extrait du site web du Vendée Globe. Elles sont ensuite traitées, nettoyées et mises en forme pour de la visualisation et des traitements statistiques dont nous présenterons les conclusions.
Dans la partie analyse et visualisation des données, nous allons entre autre travailler sur des séries temporelles, la gestion des systèmes d'information géographique (pour le monitoring du parcours des voiliers), et d'autres analyses connexes.
Dans un premier temps, les différentes bibliothèques utilisées dans la ce projet, sont importées.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime
from bs4 import BeautifulSoup
import requests
import re
import pickle
Nous réalisons une extraction de la liste des fichiers disponible, par scrapping du site www.vendeeglobe.org
html = "https://www.vendeeglobe.org/fr/classement/20210117_110000"
r = requests.get(html)
type(r.content)
content = r.content.decode('utf-8')
soup = BeautifulSoup(content)
import uuid
import xlwings as xw
# lecture/écriture d'un fichier Excel avec xlwings
def save_with_xlwings(file,output_name='{uuid.uuid1()}'):
tempfile = output_name+'.xlsx'
excel_app = xw.App(visible=False)
excel_book = excel_app.books.open(file)
excel_book.save(tempfile)
excel_book.close()
excel_app.quit()
return tempfile
En récupérant les différentes option de la liste déroulante de la page web, on obtient alors tous les suffixes des liens nous permettant ainsi d'automatiser le téléchargement des fichiers.
li_tag = soup.findAll('option')
link_suffix = []
for tag in li_tag:
if tag.attrs["value"] != "":
link_suffix.append(tag.attrs["value"])
print("Il y a au total {} fichiers de classement disponible sur le site vendée globe".format(len(link_suffix)))
Il y a au total 706 fichiers de classement disponible sur le site vendée globe
On se rend compte qu'après l'arrivé de certains voiliers, la structure des fichiers excel change. Alors nous décidons de nous en tenir aux éléments entre le 2021-11-08 et 2021-03-04.
last_date = '20210127_140000'
link_to_use = link_suffix[link_suffix.index(last_date):]
df_temp = pd.DataFrame()
for link in link_to_use:
url_excel = 'https://www.vendeeglobe.org/download-race-data/vendeeglobe_'+link+'.xlsx'
from shutil import copyfileobj
from urllib import request
filename = link+'.xlsx'
with request.urlopen(url_excel) as response, open(filename, 'wb') as out_file:
copyfileobj(response, out_file)
save_with_xlwings(filename,output_name=link)
df1 = (pd.read_excel(filename,header=3,usecols=[1,2,3,5,6,12,13,19])
.rename(columns={"Rang\nRank":"Rang","Skipper / Bateau\nSkipper / crew":"Skipper_bateau",
"Nat. / Voile\nNat. / Sail":"Voile","Unnamed: 5":"Latitude",
"Unnamed: 6":"Longitude", "Unnamed: 12":"Vitesse (kts)",
"Unnamed: 13":"VMG (kts)","DTF":"DTF (nm)"})
.drop(0)
.dropna(subset=["Vitesse (kts)"])
.reset_index(drop=True)
)
temp = datetime.strptime(link,"%Y%m%d_%H%M%S")
df1["date_format"] = temp.strftime("%Y-%m-%d %H:%M:%S")
df_temp = pd.concat([df_temp, df1])
# exampleObj = df_temp
# fileObj = open('df_temp.obj', 'wb')
# pickle.dump(exampleObj,fileObj)
# fileObj.close()
# fileObj = open('df_temp.obj', 'rb')
# df_temp = pickle.load(fileObj)
# fileObj.close()
df_temp
| Rang | Voile | Skipper_bateau | Latitude | Longitude | Vitesse (kts) | VMG (kts) | DTF (nm) | date_format | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | \nFRA 79 | Charlie Dalin\nAPIVIA | 46°14.07'N | 03°41.61'W | 19.1 kts | 17.4 kts | 80.5 nm | 2021-01-27 14:00:00 |
| 1 | 2 | \nFRA 18 | Louis Burton\nBureau Vallée 2 | 46°24.62'N | 05°11.90'W | 17.7 kts | 16.9 kts | 141.5 nm | 2021-01-27 14:00:00 |
| 2 | 3 | \nMON 10 | Boris Herrmann\nSeaexplorer - Yacht Club De Mo... | 44°31.46'N | 05°20.55'W | 16.6 kts | 10.3 kts | 190.0 nm | 2021-01-27 14:00:00 |
| 3 | 4 | \nFRA 59 | Thomas Ruyant\nLinkedOut | 47°24.42'N | 07°22.08'W | 17.6 kts | 17.6 kts | 235.8 nm | 2021-01-27 14:00:00 |
| 4 | 5 | \nFRA 17 | Yannick Bestaven\nMaître Coq IV | 47°43.38'N | 07°58.81'W | 18.1 kts | 18.1 kts | 264.3 nm | 2021-01-27 14:00:00 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 26 | 27 | \nFRA 72 | Alexia Barrier\nTSE - 4myplanet | 46°25.83'N | 01°48.92'W | 0.0 kts | 0.0 kts | 24295.4 nm | 2020-11-08 14:00:00 |
| 27 | 28 | \nFRA 27 | Isabelle Joschke\nMACSF | 46°24.98'N | 01°48.22'W | 0.0 kts | 0.0 kts | 24295.5 nm | 2020-11-08 14:00:00 |
| 28 | 29 | \nFRA 4 | Sébastien Simon\nARKEA PAPREC | 46°25.75'N | 01°48.73'W | 0.0 kts | 0.0 kts | 24295.5 nm | 2020-11-08 14:00:00 |
| 29 | 30 | \nFRA 50 | Miranda Merron\nCampagne de France | 46°25.39'N | 01°48.34'W | 0.0 kts | 0.0 kts | 24295.6 nm | 2020-11-08 14:00:00 |
| 30 | 31 | \nFIN 222 | Ari Huusela\nStark | 46°25.65'N | 01°48.21'W | 0.0 kts | 0.0 kts | 24295.8 nm | 2020-11-08 14:00:00 |
13784 rows × 9 columns
df = df_temp.copy()
df.head(5)
| Rang | Voile | Skipper_bateau | Latitude | Longitude | Vitesse (kts) | VMG (kts) | DTF (nm) | date_format | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | \nFRA 79 | Charlie Dalin\nAPIVIA | 46°14.07'N | 03°41.61'W | 19.1 kts | 17.4 kts | 80.5 nm | 2021-01-27 14:00:00 |
| 1 | 2 | \nFRA 18 | Louis Burton\nBureau Vallée 2 | 46°24.62'N | 05°11.90'W | 17.7 kts | 16.9 kts | 141.5 nm | 2021-01-27 14:00:00 |
| 2 | 3 | \nMON 10 | Boris Herrmann\nSeaexplorer - Yacht Club De Mo... | 44°31.46'N | 05°20.55'W | 16.6 kts | 10.3 kts | 190.0 nm | 2021-01-27 14:00:00 |
| 3 | 4 | \nFRA 59 | Thomas Ruyant\nLinkedOut | 47°24.42'N | 07°22.08'W | 17.6 kts | 17.6 kts | 235.8 nm | 2021-01-27 14:00:00 |
| 4 | 5 | \nFRA 17 | Yannick Bestaven\nMaître Coq IV | 47°43.38'N | 07°58.81'W | 18.1 kts | 18.1 kts | 264.3 nm | 2021-01-27 14:00:00 |
df["Voile"] = df.Voile.apply(lambda x: re.compile(r'\n(.*)').search(x).group(1))
df["Skipper"] = df.Skipper_bateau.apply(lambda x: re.compile(r'(.*)\n(.*)').search(x).group(1))
df["Bateau"] = df.Skipper_bateau.apply(lambda x: re.compile(r'(.*)\n(.*)').search(x).group(2))
df["Vitesse (kts)"] = df["Vitesse (kts)"].apply(lambda x: re.compile(r'(.*) (.*)').search(x).group(1)).astype(float)
df["VMG (kts)"] = df["VMG (kts)"].apply(lambda x: re.compile(r'(.*) (.*)').search(x).group(1)).astype(float)
df["DTF (nm)"] = df["DTF (nm)"].apply(lambda x: re.compile(r'(.*) (.*)').search(x).group(1)).astype(float)
df["Rang"] = df["Rang"].astype(int)
df = df.drop(columns="Skipper_bateau")
df = df[["date_format","Rang","Voile","Skipper","Bateau","Vitesse (kts)","VMG (kts)","DTF (nm)","Latitude","Longitude",]]
On réalise plusieurs opération de traitement de nos fichiers excels individuellement, notamment :
Puis on fait un traitement finale qui consiste à :
Le dataframe finale est présenté ci-dessous.
df
| date_format | Rang | Voile | Skipper | Bateau | Vitesse (kts) | VMG (kts) | DTF (nm) | Latitude | Longitude | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2021-01-27 14:00:00 | 1 | FRA 79 | Charlie Dalin | APIVIA | 19.1 | 17.4 | 80.5 | 46°14.07'N | 03°41.61'W |
| 1 | 2021-01-27 14:00:00 | 2 | FRA 18 | Louis Burton | Bureau Vallée 2 | 17.7 | 16.9 | 141.5 | 46°24.62'N | 05°11.90'W |
| 2 | 2021-01-27 14:00:00 | 3 | MON 10 | Boris Herrmann | Seaexplorer - Yacht Club De Monaco | 16.6 | 10.3 | 190.0 | 44°31.46'N | 05°20.55'W |
| 3 | 2021-01-27 14:00:00 | 4 | FRA 59 | Thomas Ruyant | LinkedOut | 17.6 | 17.6 | 235.8 | 47°24.42'N | 07°22.08'W |
| 4 | 2021-01-27 14:00:00 | 5 | FRA 17 | Yannick Bestaven | Maître Coq IV | 18.1 | 18.1 | 264.3 | 47°43.38'N | 07°58.81'W |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 26 | 2020-11-08 14:00:00 | 27 | FRA 72 | Alexia Barrier | TSE - 4myplanet | 0.0 | 0.0 | 24295.4 | 46°25.83'N | 01°48.92'W |
| 27 | 2020-11-08 14:00:00 | 28 | FRA 27 | Isabelle Joschke | MACSF | 0.0 | 0.0 | 24295.5 | 46°24.98'N | 01°48.22'W |
| 28 | 2020-11-08 14:00:00 | 29 | FRA 4 | Sébastien Simon | ARKEA PAPREC | 0.0 | 0.0 | 24295.5 | 46°25.75'N | 01°48.73'W |
| 29 | 2020-11-08 14:00:00 | 30 | FRA 50 | Miranda Merron | Campagne de France | 0.0 | 0.0 | 24295.6 | 46°25.39'N | 01°48.34'W |
| 30 | 2020-11-08 14:00:00 | 31 | FIN 222 | Ari Huusela | Stark | 0.0 | 0.0 | 24295.8 | 46°25.65'N | 01°48.21'W |
13784 rows × 10 columns
Egalement par scrapping, on récupère les fiches techniques de tous les bateaux
html_b = 'https://www.vendeeglobe.org/fr/glossaire'
r_b = requests.get(html_b)
content_b = r_b.content.decode('utf-8')
soup_b = BeautifulSoup(content_b)
On vérifie bien que la class "boats-list__popup-title" nous permet bien d'obtenir les noms des bateaux, en vérifiant sur un cas.
soup_b.find('h3',{'class': "boats-list__popup-title"}).text
'NEWREST - ART & FENÊTRES'
On peut à présent généraliser la récupération des informations du bateau. Ces informations sont d'abord stockées dans un dictionnaire dont les clés et les valeurs associées sont présentés ci-après.
boats_list = soup_b.find_all('h3',{'class': "boats-list__popup-title"})
dico_bateau = {}
for item in boats_list:
#print("Clé : {}".format(item.text))
dico_bateau_key = item.text
dico_temp = {}
for item_li in item.find_next().find_all('li'):
#print("\tValeur : ({})".format(item_li.text))
dico_key = re.compile(r'(.*) : (.*)').search(item_li.text).group(1)
dico_val = re.compile(r'(.*) : (.*)').search(item_li.text).group(2)
dico_temp.update({dico_key:dico_val})
dico_bateau_value = dico_temp
dico_bateau.update({dico_bateau_key:dico_bateau_value})
On crée un dictionnaire composé, dont les clés sont les noms des bateaux et les valeurs sont des dictionnaires contenant les caratéristiques des bateaux
print("Il y a au total {} bateau inscrit à la comptétion".format(len(dico_bateau)))
Il y a au total 34 bateau inscrit à la comptétion
On remarquera plus aisément dans la suite qu'il y a un bateau dont la seule information est la date de lancement. Cela nous pousse à croire en une erreur dans la base. Néanmoins, cela ne nous empêche pas de travailler.
# dico_bateau
On peut aussi constater que les noms des bateaux dans la fiches technique et dans les fichiers excels du classement ont quelques différences, que nous nous donnons pour objectifs de corriger.
Pour cela, on va construire un dicotionnaire que correspondance que nous allons par la suite mapper sur l'un des dataframes.
df_bateau = pd.DataFrame.from_dict(dico_bateau).T
df_bateau = df_bateau.reset_index()
df_bateau["Boat"] = df_bateau["index"].str.capitalize()
df["boat"] = df.Bateau.str.capitalize()
dico_map = {np.sort(df_bateau["Boat"].unique())[1:][i]:np.sort(df["boat"].unique())[i] for i in range(len(df_bateau)-1)}
dico_map
{'Apivia': 'Apivia',
'Arkea paprec': 'Arkea paprec',
'Banque populaire x': 'Banque populaire x',
'Bureau vallee 2': 'Bureau vallée 2',
'Campagne de france': 'Campagne de france',
'Charal': 'Charal',
'Compagnie du lit / jiliti': 'Compagnie du lit - jiliti',
"Corum l'epargne": "Corum l'épargne",
'Dmg mori global one': 'Dmg mori global one',
'Groupe apicil': 'Groupe apicil',
'Groupe sétin': 'Groupe sétin',
'Hugo boss': 'Hugo boss',
'Initiatives-coeur': 'Initiatives - coeur',
"L'occitane en provence": "L'occitane en provence",
'La fabrique': 'La fabrique',
'La mie câline - artisans artipôle': 'La mie câline - artisans artipôle',
'Linkedout': 'Linkedout',
'Macsf': 'Macsf',
'Maître coq iv': 'Maître coq iv',
'Medallia': 'Medallia',
'Merci': 'Merci',
'Newrest - art & fenêtres': 'Newrest - art et fenetres',
'Omia - water family ': 'Omia - water family',
'One planet one ocean': 'One planet one ocean',
'Prb': 'Prb',
'Prysmian group': 'Prysmian group',
'Pure - best western®': 'Pure - best western hotels and resorts',
'Seaexplorer - yacht club de monaco': 'Seaexplorer - yacht club de monaco',
'Stark': 'Stark',
'Time for oceans': 'Time for oceans',
'Tse - 4myplanet': 'Tse - 4myplanet',
'V and b-mayenne': 'V and b mayenne',
'Yes we cam!': 'Yes we cam !'}
La correspondance étant vérifiées, on réalise la mapping des noms sur le dataframe des caractéristique bateau. Ce choix est stratégique car c'est le dataframe le plus petit. Le temps d'exécution est donc meilleurs.
df_bateau.Boat = df_bateau.Boat.map(dico_map)
df_bateau.head(10)
| index | Numéro de voile | Anciens noms du bateau | Architecte | Chantier | Date de lancement | Longueur | Largeur | Tirant d'eau | Déplacement (poids) | Nombre de dérives | Hauteur mât | Voile quille | Surface de voiles au près | Surface de voiles au portant | Chantier : CDK Technologies / Assemblage | Boat | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | NEWREST - ART & FENÊTRES | FRA 56 | No Way Back, Vento di Sardegna | VPLP/Verdier | Persico Marine | 01 Août 2015 | 18,28 m | 5,85 m | 4,50 m | 7 t | foils | 29 m | monotype | 320 m2 | 570 m2 | NaN | NaN |
| 1 | PURE - Best Western® | FRA 49 | Gitana Eighty, Synerciel, Newrest-Matmut | Bruce Farr Design | Southern Ocean Marine (Nouvelle Zélande) | 08 Mars 2007 | 18,28m | 5,80m | 4,50m | 9t | 2 | 28m | acier forgé | 280 m2 | 560 m2 | NaN | NaN |
| 2 | TSE - 4MYPLANET | FRA72 | Famille Mary-Etamine du Lys, Initiatives Coeur... | Marc Lombard | MAG France | 01 Mars 1998 | 18,28m | 5,54m | 4,50m | 9t | 2 | 29 m | acier | 260 m2 | 580 m2 | NaN | NaN |
| 3 | Maître CoQ IV | 17 | Safran 2 - Des Voiles et Vous | Verdier - VPLP | CDK Technologies | 12 Mars 2015 | 18,28 m | 5,80 m | 4,50 m | 8 t | foils | 29 m | acier mécano soudé | 310 m2 | 550 m2 | NaN | Maître coq iv |
| 4 | CHARAL | 08 | NaN | VPLP | CDK Technologies | 18 Août 2018 | 18,28 m | 5,85 m | 4,50 m | 8t | foils | 29 m | acier | 320 m2 | 600 m2 | NaN | Charal |
| 5 | LA MIE CÂLINE - ARTISANS ARTIPÔLE | FRA 14 | Ecover3, Président, Gamesa, Kilcullen Voyager-... | Owen Clarke Design LLP - Clay Oliver | Hakes Marine - Mer Agitée | 03 Août 2007 | 18,28 m | 5,65 m | 4,50 m | 7,9 tonnes | foils | 29 m | basculante avec vérin | 300 m² | 610 m² | NaN | La mie câline - artisans artipôle |
| 6 | BUREAU VALLEE 2 | 18 | Banque Populaire VIII | Verdier - VPLP | CDK Technologies | 09 Juin 2015 | 18,28 m | 5,80 m | 4,50 m | 7,6 t | foils | 28 m | acier | 300 m2 | 600 m2 | NaN | NaN |
| 7 | ONE PLANET ONE OCEAN | ESP 33 | Kingfisher - Educacion sin Fronteras - Forum M... | Owen Clarke Design | Martens Yachts | 02 Février 2000 | 18,28 m | 5,30 m | 4,50 m | 8,9 t | 2 | 26 m | acier | 240 m2 | 470 m2 | NaN | One planet one ocean |
| 8 | GROUPE SÉTIN | FRA 71 | Paprec-Virbac2, Estrella Damm, We are Water, L... | Bruce Farr Yacht Design | Southern Ocean Marine (Nouvelle-Zélande) | 02 Février 2007 | 18,28 m | 5,80 m | 4,50 m | 9 t | 2 asymétriques | 28,50 | basculante sur vérin hydraulique | 270 m2 | 560 m2 | NaN | Groupe sétin |
| 9 | BANQUE POPULAIRE X | FRA30 | Macif - SMA | Verdier - VPLP | CDK - Mer Agitée | 01 Mars 2011 | 18,28 m | 5,70 m | 4,5 m | 7,7 t | 2 | 29 m | acier forgé | 340 m2 | 570 m2 | NaN | Banque populaire x |
df_bateau["Boat"].unique()
array(['Newrest - art et fenetres',
'Pure - best western hotels and resorts', 'Tse - 4myplanet',
'Maître coq iv', 'Charal', 'La mie câline - artisans artipôle',
'Bureau vallée 2', 'One planet one ocean', 'Groupe sétin',
'Banque populaire x', 'Apivia', 'Initiatives - coeur', 'Merci',
'Omia - water family', 'Prb', 'Compagnie du lit - jiliti', nan,
'Medallia', 'Seaexplorer - yacht club de monaco', 'Stark', 'Macsf',
'Yes we cam !', 'Time for oceans', 'Campagne de france',
'Prysmian group', 'La fabrique', 'Linkedout', 'Groupe apicil',
'Dmg mori global one', 'Arkea paprec', 'V and b mayenne',
'Hugo boss', "L'occitane en provence", "Corum l'épargne"],
dtype=object)
On vérifie bien que le mapping a été réalisé avec succès sur le dataframe.
A présent, on fait un rapprochement entre les 2 dataframes pour obtenir un unique comportant les classements et les caractéristique des bateaux associés.
df_final = (pd.merge(df,df_bateau,left_on="boat",right_on="Boat")
.reset_index(drop=True))
df_final
| date_format | Rang | Voile | Skipper | Bateau | Vitesse (kts) | VMG (kts) | DTF (nm) | Latitude | Longitude | ... | Largeur | Tirant d'eau | Déplacement (poids) | Nombre de dérives | Hauteur mât | Voile quille | Surface de voiles au près | Surface de voiles au portant | Chantier : CDK Technologies / Assemblage | Boat | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2021-01-27 14:00:00 | 1 | FRA 79 | Charlie Dalin | APIVIA | 19.1 | 17.4 | 80.5 | 46°14.07'N | 03°41.61'W | ... | 5,85 m | 4,50 m | 8 t | foils | 29 m | acier | 350 m2 | 560 m2 | NaN | Apivia |
| 1 | 2021-01-27 11:00:00 | 1 | FRA 79 | Charlie Dalin | APIVIA | 19.4 | 19.0 | 128.9 | 45°28.35'N | 04°30.86'W | ... | 5,85 m | 4,50 m | 8 t | foils | 29 m | acier | 350 m2 | 560 m2 | NaN | Apivia |
| 2 | 2021-01-27 08:00:00 | 1 | FRA 79 | Charlie Dalin | APIVIA | 19.1 | 19.1 | 184.9 | 44°44.72'N | 05°25.61'W | ... | 5,85 m | 4,50 m | 8 t | foils | 29 m | acier | 350 m2 | 560 m2 | NaN | Apivia |
| 3 | 2021-01-27 04:00:00 | 1 | FRA 79 | Charlie Dalin | APIVIA | 13.4 | 11.4 | 261.0 | 43°53.46'N | 06°44.90'W | ... | 5,85 m | 4,50 m | 8 t | foils | 29 m | acier | 350 m2 | 560 m2 | NaN | Apivia |
| 4 | 2021-01-26 21:00:00 | 1 | FRA 79 | Charlie Dalin | APIVIA | 11.2 | 11.2 | 337.8 | 43°56.62'N | 08°55.23'W | ... | 5,85 m | 4,50 m | 8 t | foils | 29 m | acier | 350 m2 | 560 m2 | NaN | Apivia |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 13779 | 2020-11-09 04:00:00 | 5 | FRA 6 | Nicolas Troussel | CORUM L'Épargne | 14.7 | 4.7 | 24137.1 | 46°30.70'N | 07°33.22'W | ... | 5,70 m | 4,50 m | 7,9 t | foils | 27,30 m | NaN | 270 m2 | 535 m2 | NaN | Corum l'épargne |
| 13780 | 2020-11-08 21:00:00 | 2 | FRA 6 | Nicolas Troussel | CORUM L'Épargne | 17.9 | 15.7 | 24179.5 | 46°14.80'N | 05°05.76'W | ... | 5,70 m | 4,50 m | 7,9 t | foils | 27,30 m | NaN | 270 m2 | 535 m2 | NaN | Corum l'épargne |
| 13781 | 2020-11-08 17:00:00 | 6 | FRA 6 | Nicolas Troussel | CORUM L'Épargne | 22.9 | 19.4 | 24239.0 | 46°22.46'N | 03°22.50'W | ... | 5,70 m | 4,50 m | 7,9 t | foils | 27,30 m | NaN | 270 m2 | 535 m2 | NaN | Corum l'épargne |
| 13782 | 2020-11-08 15:00:00 | 6 | FRA 6 | Nicolas Troussel | CORUM L'Épargne | 19.6 | 18.2 | 24267.4 | 46°21.97'N | 02°32.77'W | ... | 5,70 m | 4,50 m | 7,9 t | foils | 27,30 m | NaN | 270 m2 | 535 m2 | NaN | Corum l'épargne |
| 13783 | 2020-11-08 14:00:00 | 20 | FRA 6 | Nicolas Troussel | CORUM L'Épargne | 0.0 | 0.0 | 24295.2 | 46°25.80'N | 01°49.19'W | ... | 5,70 m | 4,50 m | 7,9 t | foils | 27,30 m | NaN | 270 m2 | 535 m2 | NaN | Corum l'épargne |
13784 rows × 28 columns
print(len(df_final)-len(df))
0
On vérifie bien qu'entre le dataframe original (df) et le dataframe finale comportant les caractéristique du bateau (df_final) on a le même nombre de ligne.
Il n'y a donc pas de perte d'information.
C'est dans cette partie que nous nous focalisons sur les informations que nous pouvons tirer de notre jeu de données.
Dans un premier temps, nous réalisons l'analyse de la présence du Foil sur le classement et la vitesse des voiliers.
Pour ce faire, nous faisons quelques traitement supplémentaire pour se construire spécifique. Nous partons du dataframe final, supprimons des colonnes non essentiel et en rajoutons certaines, comme celle de la présence du foils.
Nous pouvons remarqué, comme présenté ci-dessous, que le foil est décrit par la caractéristique du nombre de dérives. En observant les l'ensemble des éléments on peut donc créer par mapping une nouvelle colonne ayant "Oui" si le voilier est équipé d'un foil ou "Non" si ce n'est pas le cas.
df_last = df_final.copy()
df_last["Nombre de dérives"].unique()
array(['foils', '2', '2 asymétriques', 'foiler'], dtype=object)
df_last = df_final.copy()
df_last["foils"] = df_last["Nombre de dérives"].map({"foils":"Oui",'2':"Non",'2 asymétriques':"Non",'foiler':"Non"})
df_last["foils_colors"] = df_last["Nombre de dérives"].map({"foils":"Blue",'2':"Red",'2 asymétriques':"Red",'foiler':"Red"})
df_last = df_last.loc[df_final.date_format == '2021-01-27 11:00:00']
df_last = df_last.reset_index(drop=True)
df_last_filter = df_last[["Rang","Skipper","Bateau","VMG (kts)","foils","foils_colors"]]
Pour la forme on rajoute également une colonne couleur qui dans la visualisation nous permettra de différencier les voiliers ayant ou pas le foil. Le résultat est présencté dans la figure ci-dessous. On restreint l'analyse au 27 janvier 2021 à 11h.
fig,ax = plt.subplots(figsize=(8,6))
x1 = df_last_filter.loc[df_last_filter["foils_colors"]=="Blue"]
x2 = df_last_filter.loc[df_last_filter["foils_colors"]=="Red"]
ax.scatter(x1["Rang"],x1["VMG (kts)"],c=x1["foils_colors"])
ax.scatter(x2["Rang"],x2["VMG (kts)"],c=x2["foils_colors"])
plt.xlabel("Rang")
plt.ylabel("Vitesse Utile (kts)")
plt.legend(["Avec Foil","Sans Foil"])
ax.axvline(x=5.5, ymin=0, linewidth=1, color="k", linestyle = '--')
ax.axvline(x=18.5, ymin=0, linewidth=1, color="k", linestyle = '--')
plt.show;
Il se forme une sorte de cluster. On remarque que les 5 premiers possède un foil, les 6 derniers n'en possède pas, et au milieu de ceux la, on a une zone d'incertitude.
Le foil serait donc une caractéristique importante qui donnerait plus de chance aux voiliers de remporter la course.
Nous poursuivons par une analyse de séries temporelles.
Dans le premier exemple, nous suivons l'évolution dans le temps de rang des 2 premiers skippers.
On extrait donc de notre dataframe final, les informations relatives aux skippers ciblés.
df0_temp = df_final.loc[df_final["Skipper"] == df["Skipper"].unique()[0]]
df1_temp = df_final.loc[df_final["Skipper"] == df["Skipper"].unique()[1]]
df0_temp = df0_temp.sort_values(by=["date_format"], ascending=True)
temp = df0_temp.date_format.apply(lambda x: datetime.strptime(x,"%Y-%m-%d %H:%M:%S"))
df1_temp = df1_temp.sort_values(by=["date_format"], ascending=True)
temp = df1_temp.date_format.apply(lambda x: datetime.strptime(x,"%Y-%m-%d %H:%M:%S"))
fig,ax = plt.subplots(figsize=(8,6))
ax.scatter(df0_temp.date_format,df0_temp["Rang"])
ax.scatter(df1_temp.date_format[1:],df1_temp["Rang"][1:])
plt.xticks(df0_temp.date_format[::25])
plt.xticks(rotation = 45, ha="right")
plt.ylabel('Rang')
plt.legend([df0_temp.Skipper.iloc[0],df1_temp.Skipper.iloc[0]])
ax.axhline(y = 1, linestyle = '--',color="k")
plt.show;
Dans ce graphique, la première place est mise en évidence par le trait horizontal noir.
Remarque : La fin de la course est vers la gauche du graphique et le début vers la droite
On constate au départ qu'il y a une grosse incertitude sur le positionnement; et à mesure que le temps passe, on voit bien que le skipper Chalie Dalin conserve son avantage, une majeur partie du temps.
Dans le deuxième exemple, on observe l'évolution de la vitesse de Charlie Dalin au cours du temps. Comme le montre le graphique ci-dessous, on ne peut pas tirer de conclusion, ci ce n'est que la vitesse varie en moyenne autre de 14.71 kts
print("la vitesse moyenne de Charlie Dalin est de : {:.2f}".format(df0_temp["Vitesse (kts)"].mean()))
la vitesse moyenne de Charlie Dalin est de : 14.71
fig,ax = plt.subplots(figsize=(8,6))
temp = df0_temp.date_format.apply(lambda x: datetime.strptime(x,"%Y-%m-%d %H:%M:%S"))
plt.plot(temp.apply(lambda x: x.strftime("%d-%m-%Y")),df0_temp["Vitesse (kts)"])
plt.xticks(temp.apply(lambda x: x.strftime("%d-%m-%Y"))[::25]);
plt.yticks(np.arange(min(df0_temp["Vitesse (kts)"]), max(df0_temp["Vitesse (kts)"])+1, 2.0))
plt.ylabel('Vitesse (kts)')
plt.legend({df0_temp.Skipper.iloc[0]})
plt.xticks(rotation = 45, ha="right");
A présent, nous nous intéressons au parcours géographique des voiliers. Nous allons donc présenté dans le parcours d'un skipper
Nous disposons dans notre dataframe des coordonnées GPS (latitude, longitude) en format degré minute seconde. Nous les convertissons en nombre décimaux pour pouvoir les afficher dans la carte.
On crée donc de nouvelles colonnes comportant les coordonnées décimales des positions du voilier au file du temps.
E = "E"
W = "W"
N = "N"
S = "S"
def dms2dec(deg, mn, sec,sens):
if sens == "W" or sens == "S":
poid = -1
else:
poid = 1
nbre = (deg + mn/60 + sec/3600)*poid
return nbre
df_final["lon_split"] = df_final.Longitude.apply(lambda x: tuple(re.split("°|\.|'",x)))
df_final["coor_y"] = df_final.lon_split.apply(lambda x: dms2dec(deg=int(str(x[0])),mn=int(str(x[1])),sec=int(str(x[2])),sens=x[3]))
df_final["lat_split"] = df_final.Latitude.apply(lambda x: tuple(re.split("°|\.|'",x)))
df_final["coor_x"] = df_final.lat_split.apply(lambda x: dms2dec(deg=int(str(x[0])),mn=int(str(x[1])),sec=int(str(x[2])),sens=x[3]))
print("On retrace le parcours du skipper : {}".format(df["Skipper"].unique()[0]))
print("Ces information sont présentés ci-bas")
On retrace le parcours du skipper : Charlie Dalin Ces information sont présentés ci-bas
Une fois de dataframe traiter, nous pouvons réaliser la visualisation grâce à l'API Pyplot. Le résultat du parcours du skipper peut être observé dans l'image qui suit.
df_final.loc[(df_final["Skipper"] == df["Skipper"].unique()[0]) & (df_final["Skipper"] == df["Skipper"].unique()[0])]
| date_format | Rang | Voile | Skipper | Bateau | Vitesse (kts) | VMG (kts) | DTF (nm) | Latitude | Longitude | ... | Hauteur mât | Voile quille | Surface de voiles au près | Surface de voiles au portant | Chantier : CDK Technologies / Assemblage | Boat | lon_split | coor_y | lat_split | coor_x | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2021-01-27 14:00:00 | 1 | FRA 79 | Charlie Dalin | APIVIA | 19.1 | 17.4 | 80.5 | 46°14.07'N | 03°41.61'W | ... | 29 m | acier | 350 m2 | 560 m2 | NaN | Apivia | (03, 41, 61, W) | -3.700278 | (46, 14, 07, N) | 46.235278 |
| 1 | 2021-01-27 11:00:00 | 1 | FRA 79 | Charlie Dalin | APIVIA | 19.4 | 19.0 | 128.9 | 45°28.35'N | 04°30.86'W | ... | 29 m | acier | 350 m2 | 560 m2 | NaN | Apivia | (04, 30, 86, W) | -4.523889 | (45, 28, 35, N) | 45.476389 |
| 2 | 2021-01-27 08:00:00 | 1 | FRA 79 | Charlie Dalin | APIVIA | 19.1 | 19.1 | 184.9 | 44°44.72'N | 05°25.61'W | ... | 29 m | acier | 350 m2 | 560 m2 | NaN | Apivia | (05, 25, 61, W) | -5.433611 | (44, 44, 72, N) | 44.753333 |
| 3 | 2021-01-27 04:00:00 | 1 | FRA 79 | Charlie Dalin | APIVIA | 13.4 | 11.4 | 261.0 | 43°53.46'N | 06°44.90'W | ... | 29 m | acier | 350 m2 | 560 m2 | NaN | Apivia | (06, 44, 90, W) | -6.758333 | (43, 53, 46, N) | 43.896111 |
| 4 | 2021-01-26 21:00:00 | 1 | FRA 79 | Charlie Dalin | APIVIA | 11.2 | 11.2 | 337.8 | 43°56.62'N | 08°55.23'W | ... | 29 m | acier | 350 m2 | 560 m2 | NaN | Apivia | (08, 55, 23, W) | -8.923056 | (43, 56, 62, N) | 43.950556 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 479 | 2020-11-09 08:00:00 | 25 | FRA 79 | Charlie Dalin | APIVIA | 13.3 | 0.0 | 24147.6 | 46°54.58'N | 08°16.41'W | ... | 29 m | acier | 350 m2 | 560 m2 | NaN | Apivia | (08, 16, 41, W) | -8.278056 | (46, 54, 58, N) | 46.916111 |
| 480 | 2020-11-09 04:00:00 | 17 | FRA 79 | Charlie Dalin | APIVIA | 14.9 | 3.6 | 24146.9 | 46°35.71'N | 07°14.42'W | ... | 29 m | acier | 350 m2 | 560 m2 | NaN | Apivia | (07, 14, 42, W) | -7.245000 | (46, 35, 71, N) | 46.603056 |
| 481 | 2020-11-08 21:00:00 | 5 | FRA 79 | Charlie Dalin | APIVIA | 14.9 | 13.0 | 24185.7 | 46°10.33'N | 04°47.42'W | ... | 29 m | acier | 350 m2 | 560 m2 | NaN | Apivia | (04, 47, 42, W) | -4.795000 | (46, 10, 33, N) | 46.175833 |
| 482 | 2020-11-08 17:00:00 | 2 | FRA 79 | Charlie Dalin | APIVIA | 23.3 | 20.6 | 24235.6 | 46°15.92'N | 03°21.99'W | ... | 29 m | acier | 350 m2 | 560 m2 | NaN | Apivia | (03, 21, 99, W) | -3.377500 | (46, 15, 92, N) | 46.275556 |
| 483 | 2020-11-08 15:00:00 | 2 | FRA 79 | Charlie Dalin | APIVIA | 0.0 | 0.0 | 24265.8 | 46°17.46'N | 02°31.45'W | ... | 29 m | acier | 350 m2 | 560 m2 | NaN | Apivia | (02, 31, 45, W) | -2.529167 | (46, 17, 46, N) | 46.296111 |
484 rows × 32 columns
import plotly.graph_objects as go
df0 = df_final.loc[(df_final["Skipper"] == df["Skipper"].unique()[0])]
fig = go.Figure(data=go.Scattergeo(
lat = df0['coor_x'],
lon = df0['coor_y'],
mode = 'markers',
marker_color = df0['Skipper'].apply(lambda x: hash(x)),
text = "Rang" + " : " + str(df0['Rang'][0]) + " " + df0['Skipper'][0] + ", " + df0['Bateau'][0] + ", " + str(df0['VMG (kts)'][0]) + " kts"
))
fig.update_layout(
geo = dict(
projection_type="natural earth",#"natural earth","orthographic"
showland = True, landcolor = "rgb(245, 250, 220)",
showocean=True, oceancolor="rgb(210, 255, 255)",#"rgb(210, 255, 255)"
showlakes = True, lakecolor = "rgb(240, 255, 255)",
showcountries=True, countrycolor="Black",
lonaxis = dict(
showgrid = True,
dtick = 5
),
lataxis = dict (
showgrid = True,
dtick = 5
)
),
)
fig.update_layout(height=500, margin={"r":0,"t":0,"l":0,"b":0})
fig.show()
Cette visualisation vient donc terminer le notre étude sur le jeu de données du Vendée Globe 2020-2021